#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

#include <ctype.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#include <pthread.h>
#include <string.h>
#define __USE_GNU
#include <sys/time.h>

#include "logf_ips.h"
//#include "logd_error.h"

//#include "mtd_common.h"
#include "mtd_extensions_geodb.h"

#define CONTINENT_FILE "/secui/etc/country_continent.csv"
#define COUNTRY_FILE4 "/secui/etc/geoipcountrywhois.csv"
#define COUNTRY_FILE6 "/secui/etc/geoipcountrywhoisv6.csv"

struct hash_tables m_hash_tables;
struct geodb m_geodb;

int add_entry_to_continent(struct geodb *pGeoDB, char *pCountry, char *pContinent)
{
    return 0;
}

int add_entry_to_ipv4(struct geodb *pGeoDB, char *pRangeStart, char *pRangeEnd, char *pCountry)
{
    // printf("In add_entry_to_ipv4(pGeoDB, %s, %s, %s)\n", pRangeStart, pRangeEnd, pCountry);
    if (pGeoDB->ipv4_use >= pGeoDB->ipv4_size) {
        pGeoDB->ipv4_size *= 2;
        struct ipv4_entry *pNewTable = (struct ipv4_entry *)realloc(pGeoDB->p_ipv4_table, 
                                                                    pGeoDB->ipv4_size * sizeof(struct ipv4_entry));
        if (pNewTable == NULL) {
            exit(1);
        }
        bzero(pNewTable + pGeoDB->ipv4_use, pGeoDB->ipv4_size - pGeoDB->ipv4_use);
        pGeoDB->p_ipv4_table = pNewTable;
    }
    struct ipv4_entry *pEntry = pGeoDB->p_ipv4_table + pGeoDB->ipv4_use;
    pEntry->range_start = strtoul(pRangeStart, NULL, 10);
    pEntry->range_end = strtoul(pRangeEnd, NULL, 10);
    strncpy(pEntry->country_code, pCountry, 2);
    ++pGeoDB->ipv4_use;
    return 0;
}

int add_entry_to_ipv6(struct geodb *pGeoDB, char *pRangeStart, char *pRangeEnd, char *pCountry)
{
    //printf("In add_entry_to_ipv6(pGeoDB, %s, %s, %s)\n", pRangeStart, pRangeEnd, pCountry);
    if (pGeoDB->ipv6_use >= pGeoDB->ipv6_size) {
        pGeoDB->ipv6_size *= 2;
        struct ipv6_entry *pNewTable = (struct ipv6_entry *)realloc(pGeoDB->p_ipv6_table, 
                                                                    pGeoDB->ipv6_size * sizeof(struct ipv6_entry));
        if (pNewTable == NULL) {
            exit(1);
        }
        bzero(pNewTable + pGeoDB->ipv6_use, pGeoDB->ipv6_size - pGeoDB->ipv6_use);
        pGeoDB->p_ipv6_table = pNewTable;
    }
    struct ipv6_entry *pEntry = pGeoDB->p_ipv6_table + pGeoDB->ipv6_use;
    if (inet_pton(AF_INET6, pRangeStart, &pEntry->range_start) != 1)
        return 1;
    if (inet_pton(AF_INET6, pRangeEnd, &pEntry->range_end) != 1)
        return 2;
    strncpy(pEntry->country_code, pCountry, 2);
    ++pGeoDB->ipv6_use;
    return 0;
}

/* ipv4_addr: host order */
int lookup_ipv4_table(struct geodb *pGeoDB, u_int32_t ipv4_addr, char *country_code, int len)
{
    int lower_bound, upper_bound, middle;

    lower_bound = 0;
    upper_bound = pGeoDB->ipv4_use;
    struct ipv4_entry *pTable = pGeoDB->p_ipv4_table;
    while (lower_bound != upper_bound && lower_bound + 1 != upper_bound) {
        middle = (lower_bound + upper_bound) / 2;
        if (pTable[middle].range_start <= ipv4_addr && pTable[middle].range_end >= ipv4_addr) {
            // printf("Country code for %s = %s\n", target, pTable[middle].country_code);
            strncpy(country_code, pTable[middle].country_code, len);
            return 1;
        } else if (pTable[middle].range_start > ipv4_addr) {
            upper_bound = middle;
        } else {
            lower_bound = middle;
        }
    }
    return 0;
}

int lookup_ipv4_table_with_string(struct geodb *pGeoDB, char *target, char *country_code, int len)
{
    u_int32_t ipv4_addr;

    if (inet_pton(AF_INET, target, &ipv4_addr) != 1)
        return -1;
    ipv4_addr = ntohl(ipv4_addr);
    return lookup_ipv4_table(pGeoDB, ipv4_addr, country_code, len);
}

/* for verifying */
int lookup_ipv4_table_with_string_linear(struct geodb *pGeoDB, char *target, char *country_code, int len)
{
    int i, found = 0, count = 0, range = pGeoDB->ipv4_use;
    struct ipv4_entry *pTable = pGeoDB->p_ipv4_table;
    u_int32_t ipv4_addr;

    if (inet_pton(AF_INET, target, &ipv4_addr) != 1)
        return -1;
    ipv4_addr = ntohl(ipv4_addr);
    for (i = 0; i < range; ++i) {
        if (pTable[i].range_start <= ipv4_addr && pTable[i].range_end >= ipv4_addr) {
            ++count;
            found = i;
        }
    }
    if (count)
        strncpy(country_code, pTable[found].country_code, len);
    return count;
}

int lookup_ipv6_table(struct geodb *pGeoDB, struct in6_addr *p_ipv6_addr, char *country_code, int len)
{
    int lower_bound, upper_bound, middle;

    lower_bound = 0;
    upper_bound = pGeoDB->ipv6_use;
    struct ipv6_entry *pTable = pGeoDB->p_ipv6_table;
    while (lower_bound != upper_bound && lower_bound + 1 != upper_bound) {
        middle = (lower_bound + upper_bound) / 2;
        /*
        char debug_buffer1[64], debug_buffer2[64];
        inet_ntop(AF_INET6, &pTable[middle].range_start, debug_buffer1, 64);
        inet_ntop(AF_INET6, &pTable[middle].range_end, debug_buffer2, 64);
        printf("%d %d %dCheck target %s in range of middle (%s, %s)\n", 
               lower_bound, middle, upper_bound,
               target, debug_buffer1, debug_buffer2);
        */
        if (comp_ipv6(&pTable[middle].range_start, p_ipv6_addr) <= 0 
            && comp_ipv6(&pTable[middle].range_end, p_ipv6_addr) >= 0) {
            strncpy(country_code, pTable[middle].country_code, len);
            // printf("Country code for %s = %s\n", target, pTable[middle].country_code);
            return 1;
        } else if (comp_ipv6(&pTable[middle].range_start, p_ipv6_addr) > 0) {
            upper_bound = middle;
        } else {
            lower_bound = middle;
        }
    }
    return 0;
}

/* country_code should be at least 3 bytes long */
int lookup_ipv6_table_with_string(struct geodb *pGeoDB, char *target, char *country_code, int len)
{
    struct in6_addr ipv6_addr;

    if (inet_pton(AF_INET6, target, &ipv6_addr) != 1)
        return -1;

    return lookup_ipv6_table(pGeoDB, &ipv6_addr, country_code, len);
}

/* linear search for verifying */
int lookup_ipv6_table_with_string_linear(struct geodb *pGeoDB, char *target, char *country_code, int len)
{
    return 0;
}

#define LINEBUF_LEN 256
// 대륙 정보를 loading -- 이건 필요한가?
int load_continent_info(struct geodb *pGeoDB)
{
    FILE *pCont = NULL;
    char line[LINEBUF_LEN];

    pCont = fopen(CONTINENT_FILE, "r");
    if (pCont != NULL) {
        while (fgets(line, LINEBUF_LEN - 1, pCont) != NULL) {
            if (line[2] != ',' || line[5] != '\n')
                continue;
            line[2] = '\0';
            line[5] = '\0';
            add_entry_to_continent(pGeoDB, line, line + 3);
        }
        fclose(pCont);
    }
    return 0;
}

// 일단 빈 함수
int leave_parsing_fail_log(int line_num, char *line, int column, int source_line, const char *message)
{
    printf("ERR: %d %s %d %d %s\n", line_num, line, column, source_line, message);
    return 0;
}

int common_table_parser(char *filename, 
                        int (*parser_func)(struct geodb *pGeoDB, char *pRangeStart, char *pRangeEnd, char *pCountry))
{
    return 0;
}

// IPv4 국가정보를 loading
// "1.0.0.0","1.0.0.255","16777216","16777471","AU","Australia"
// 3, 4, 5 column을 사용
int load_ipv4_geoip_table(struct geodb *pGeoDB)
{
    FILE *pGeoIPv4 = NULL;
    char line[LINEBUF_LEN], work_buf[LINEBUF_LEN];

    pGeoIPv4 = fopen(COUNTRY_FILE4, "r");
    if (pGeoIPv4 != NULL) {
        int line_num = 0;
        char *pComma, *pRangeStart, *pRangeEnd, *pCountry;
        while (fgets(line, LINEBUF_LEN - 1, pGeoIPv4) != NULL) {
            // 로그용으로 원본은 보존
            ++line_num;
            strncpy(work_buf, line, LINEBUF_LEN - 1);

            pComma = strchr(work_buf, ',');
            if (pComma == NULL) {
                leave_parsing_fail_log(line_num, line, pComma - work_buf, __LINE__, 
                                       "Cannot find delimeter(,).");
                continue;
            }

            pComma = strchr(pComma + 1, ',');
            if (pComma == NULL) {
                leave_parsing_fail_log(line_num, line, pComma - work_buf, __LINE__, 
                                       "Cannot find delimeter(,).");
                continue;
            }

            while (*pComma != '\0' && !isdigit(*pComma))
                ++pComma;
            if (*pComma == '\0') {
                leave_parsing_fail_log(line_num, line, pComma - work_buf, __LINE__, 
                                       "Cannot find range start digit.");
                continue;
            }
            pRangeStart = pComma;

            pComma = strchr(pComma, ',');
            if (pComma == NULL) {
                leave_parsing_fail_log(line_num, line, pComma - work_buf, __LINE__,
                                       "Cannot find delimeter(,).");
                continue;
            }
            *(pComma - 1) = '\0';

            while (*pComma != '\0' && !isdigit(*pComma))
                ++pComma;
            if (*pComma == '\0') {
                leave_parsing_fail_log(line_num, line, pComma - work_buf, __LINE__,
                                       "Cannot find range end digit.");
                continue;
            }
            pRangeEnd = pComma;

            pComma = strchr(pComma, ',');
            if (pComma == NULL) {
                leave_parsing_fail_log(line_num, line, pComma - work_buf, __LINE__,
                                       "Cannot find delimeter(,).");
                continue;
            }
            *(pComma - 1) = '\0';

            while (*pComma != '\0' && ! isupper(*pComma))
                ++pComma;
            if (*pComma == '\0') {
                leave_parsing_fail_log(line_num, line, pComma - work_buf, __LINE__,
                                       "Cannot find coutry letter.");
                continue;
            }
            
            pCountry = pComma;
            pComma = strchr(pComma, ',');
            if (pComma == NULL) {
                leave_parsing_fail_log(line_num, line, pComma - work_buf, __LINE__,
                                       "Cannot find delimeter(,).");
                continue;
            }
            *(pComma - 1) = '\0';
            add_entry_to_ipv4(pGeoDB, pRangeStart, pRangeEnd, pCountry);
        }
        fclose(pGeoIPv4);
    }
    return 0;
}

// IPv6 국가정보를 loading
// "2001:200::", "2001:200:ffff:ffff:ffff:ffff:ffff:ffff", "42540528726795050063891204319802818560", "42540528806023212578155541913346768895", "JP", "Japan"
// 마찬가지로 1, 2, 5 column을 사용
// 주소 변환함수를 사용해야 하므로 IPv4에 비해서 많이 느릴 것으로 예상됨.
int load_ipv6_geoip_table(struct geodb *pGeoDB)
{
    FILE *pGeoIPv6 = NULL;
    char line[LINEBUF_LEN], work_buf[LINEBUF_LEN];
    int res;

    pGeoIPv6 = fopen(COUNTRY_FILE6, "r");
    if (pGeoIPv6 != NULL) {
        int line_num = 0;
        char *pComma, *pRangeStart, *pRangeEnd, *pCountry;
        while (fgets(line, LINEBUF_LEN - 1, pGeoIPv6) != NULL) {
            ++line_num;
            strncpy(work_buf, line, LINEBUF_LEN - 1);

            pRangeStart = work_buf;
            while (*pRangeStart != '\0' && *pRangeStart != ':' && !isxdigit(*pRangeStart))
                ++pRangeStart;
            if (*pRangeStart == '\0') {
                leave_parsing_fail_log(line_num, line, pRangeStart - work_buf, __LINE__,
                                       "Cannot find first valid char for beginning IPv6 address.");
                continue;
            }

            pComma = strchr(pRangeStart, ',');
            if (pComma == NULL) {
                leave_parsing_fail_log(line_num, line, pComma - work_buf, __LINE__,
                                       "Cannot find delimeter(,).");
                continue;
            }
            *(pComma - 1) = '\0';

            pRangeEnd = pComma;
            while (*pRangeEnd != '\0' && *pRangeEnd != ':' && !isxdigit(*pRangeEnd))
                ++pRangeEnd;
            if (*pRangeEnd == '\0') {
                leave_parsing_fail_log(line_num, line, pRangeEnd - work_buf, __LINE__,
                                       "Cannot find first valid char for closing IPv6 address.");
                continue;
            }

            pComma = strchr(pRangeEnd, ',');
            if (pComma == NULL) {
                leave_parsing_fail_log(line_num, line, pComma - work_buf, __LINE__,
                                       "Cannot find delimeter(,).");
                continue;
            }
            *(pComma - 1) = '\0';
            
            pComma = strchr(pComma + 1, ',');
            if (pComma == NULL)
                continue;
            pComma = strchr(pComma + 1, ',');
            if (pComma == NULL)
                continue;

            pCountry = pComma + 3;
            pComma = strchr(pCountry, ',');
            if (pComma == NULL)
                continue;
            *(pComma - 1) = '\0';
            if ((res = add_entry_to_ipv6(pGeoDB, pRangeStart, pRangeEnd, pCountry)) != 0) {
                leave_parsing_fail_log(line_num, line, res, __LINE__,
                                       "Failed to add entry to ipv6 table.");
                continue;
            }
        }
        fclose(pGeoIPv6);
    }
    return 0;
}

int init_hash_tables(void)
{
    m_hash_tables.p_table_stacking = &m_hash_tables.table1;
    m_hash_tables.p_table_flushing = &m_hash_tables.table2;
    bzero(m_hash_tables.p_table_stacking, sizeof(struct hash_table));
    bzero(m_hash_tables.p_table_flushing, sizeof(struct hash_table));
    pthread_mutex_init(&m_hash_tables.hash_mutex, NULL);

    return 0;
}

int init_geodb_statistics_ips_detect(struct geodb *pGeoDB)
{
    init_hash_tables();

    if (pGeoDB == NULL)
        pGeoDB = &m_geodb;

    bzero(pGeoDB, sizeof(struct geodb));
    pGeoDB->continent_size = 100;

    pGeoDB->ipv4_size = 512;
    pGeoDB->ipv4_use = 0;
    pGeoDB->p_ipv4_table = (struct ipv4_entry *)malloc(sizeof(struct ipv4_entry) * pGeoDB->ipv4_size);
    if (pGeoDB->p_ipv4_table == NULL) {
        // MTD_ERROR_LOG("failed to allocate pGeoDB->p_ipv4_table\n");
        exit(1);
    }

    pGeoDB->ipv6_size = 512;
    pGeoDB->ipv6_use = 0;
    pGeoDB->p_ipv6_table = (struct ipv6_entry *)malloc(sizeof(struct ipv6_entry) * pGeoDB->ipv6_size);
    if (pGeoDB->p_ipv6_table == NULL) {
        // MTD_ERROR_LOG("failed to allocate pGeoDB->p_ipv6_table\n");
        exit(1);
    }

    return 0;
}

void swap_table(void)
{
    pthread_mutex_lock(&m_hash_tables.hash_mutex);
    if (m_hash_tables.p_table_stacking == &m_hash_tables.table1) {
        m_hash_tables.p_table_stacking = &m_hash_tables.table2;
        m_hash_tables.p_table_flushing = &m_hash_tables.table1;
    }
    else {
        m_hash_tables.p_table_stacking = &m_hash_tables.table1;
        m_hash_tables.p_table_flushing = &m_hash_tables.table2;
    }
    pthread_mutex_unlock(&m_hash_tables.hash_mutex);
}

int comp_ipv6(struct in6_addr *pA, struct in6_addr *pB)
{
    int i;
    for (i = 0; i < 4; ++i)
        if (ntohl(pA->s6_addr32[i]) > ntohl(pB->s6_addr32[i]))
            return 1;
        else if (ntohl(pA->s6_addr32[i]) < ntohl(pB->s6_addr32[i]))
            return -1;
    return 0;
}

// 비교함수에 의해서 key가 결정되도록 하여 key 변경으로 logic이 바뀌는 일은 없도록 한다.
// if p_A > p_B : return 1
// if p_A < p_B : return -1
// if p_A == p_B : return 0
int comp_nodes(struct hash_node_entry *p_A, struct hash_node_entry *p_B)
{
    // 공격자 IP에 우선순위를 준다.
    int r = comp_ipv6(&p_A->attacker_ip, &p_B->attacker_ip);
    if (r == 0)
        return comp_ipv6(&p_A->victim_ip, &p_B->victim_ip);
    else
        return r;
}

// log에서 정보를 추출해서 추가될 node를 만든다.
int extract_info_from_ips_detect(struct hash_node_entry *p_node, struct logf_act_ips_detect_3 *p_log)
{
    p_node->attacker_ip = p_log->attacker_ip;
    p_node->victim_ip = p_log->victim_ip;
    p_node->byte_count = p_log->byte_cnt;
    p_node->attack_type = p_log->attack_id;
    p_node->app_id = 0;
    p_node->packet_count = p_log->pkt_cnt;
    p_node->session_count = 1;
    /* 여긴 지금 필요없을 듯. 나중에 DB에 기록될 때만 되면 될 듯.
    p_node->attacker_country_code[0] = p_node->attacker_country_code[1] = p_node->attacker_country_code[2] = 0;
    p_node->victim_country_code[0] = p_node->victim_country_code[1] = p_node->victim_country_code[2] = 0;
    */
    return 0;
}

// node가 들어갈 hash의 index를 계산.
int calc_hash(struct hash_node_entry *p_node)
{
    u_int32_t val = (p_node->attacker_ip.s6_addr32[0] + p_node->attacker_ip.s6_addr32[1] 
                     + p_node->attacker_ip.s6_addr32[2] + p_node->attacker_ip.s6_addr32[3]
                     + p_node->victim_ip.s6_addr32[0] + p_node->victim_ip.s6_addr32[1]
                     + p_node->victim_ip.s6_addr32[2] + p_node->victim_ip.s6_addr32[3]) % HASH_TABLE_SIZE;
    return val;
}

// 추가되는 노드의 값을 hash 내의 노드에 더해준다.
int add_value_to_hash_node(struct hash_node_entry *p_hash_node, struct hash_node_entry *p_insert_node)
{
    p_hash_node->byte_count += p_insert_node->byte_count;
    p_hash_node->packet_count += p_insert_node->packet_count;
    p_hash_node->session_count += p_insert_node->session_count;

    return 0;
}

// node를 해시테이블에 추가한다.
int insert_entry_to_hash(struct hash_node_entry **pp_node)
{
    
    if (pp_node == NULL)
        return -1;

    struct hash_node_entry *p_insert_node = *pp_node;
    if (p_insert_node == NULL)
        return -2;
    
    // if new_entry, set *pp_node = NULL
    // if exist entry, let *pp_node as-is in order to recycle the node.

    int slotnum = calc_hash(p_insert_node);
 
    pthread_mutex_lock(&m_hash_tables.hash_mutex);
    struct hash_node_entry **p_table = m_hash_tables.p_table_stacking->hash_table;
    if (p_table[slotnum] == NULL) {
        // insert without comparison
        p_table[slotnum] = p_insert_node;
        *pp_node = NULL;
        ++m_hash_tables.p_table_stacking->hash_table_count;
    } else {
        struct hash_node_entry *p_hash_node = p_table[slotnum];
        while (p_hash_node != NULL) {
            if (comp_nodes(p_insert_node, p_hash_node) == 0) {
                // found
                add_value_to_hash_node(p_hash_node, p_insert_node);
                break;
            }
            p_hash_node = p_hash_node->next;
        }
        if (p_hash_node == NULL) {
            p_insert_node->next = p_table[slotnum];
            p_table[slotnum] = p_insert_node;
            *pp_node = NULL;
            ++m_hash_tables.p_table_stacking->hash_table_count;
        }
    }
    pthread_mutex_unlock(&m_hash_tables.hash_mutex);
    return 0;
}

int update_geodb_statistics_ips_detect(void *p_log, size_t log_size)
{
    // DPRINTF("In update_geodb_statistics_ips_detect\n");
    static struct hash_node_entry *p_entry = NULL;
    // struct logf_act_ips_detect_3 *p_ips_detect = (struct logf_act_ips_detect_3 *)p_log;
    int res;

    if (p_log == NULL)  {
        
        return ERROR_BAD_PARAMETER;
    }

    if (p_entry == NULL) {
        p_entry = (struct hash_node_entry *)malloc(sizeof(struct hash_node_entry));
        if (p_entry == NULL) {
            return ERROR_MALLOC;
        }
        p_entry->next = NULL;
    }

    res = extract_info_from_ips_detect(p_entry, p_log);
    res = insert_entry_to_hash(&p_entry);
    // DPRINTF("return from insert_entry_to_hash = %d\n", res);
    DPRINTF("m_hash_tables.p_table_stacking->hash_table_count = %d\n",
            m_hash_tables.p_table_stacking->hash_table_count);

    return 0;
}

void *flush_geodb_table(void *p_args)
{

    struct timeval on_start, on_end, diff;
    struct timespec sleep, remain;
    int r, i, node_count;

    //pthread_detach(pthread_self());

    while (1) {
        // on processing start
        gettimeofday(&on_start, NULL);
        
        swap_table();
        
        DPRINTF("m_hash_tables.p_table_flushing->hash_table_count = %d\n", 
               m_hash_tables.p_table_flushing->hash_table_count);

        struct hash_node_entry **p_sort_table;
        p_sort_table = (struct hash_node_entry **)malloc(sizeof(struct hash_node_entry *)
                                                         * m_hash_tables.p_table_flushing->hash_table_count);
                                                                                  
        node_count = 0;
        struct hash_node_entry **p_head = m_hash_tables.p_table_flushing->hash_table, *p_node, *old_node;
        for (i = 0; i < HASH_TABLE_SIZE; ++i) {
            p_node = p_head[i];
            while (p_node != NULL) {

                if (p_sort_table != NULL) {
                    /* use this one */
                    p_sort_table[node_count] = p_node;
                }
                ++node_count;
                
                p_node = p_node->next;
            }
            p_head[i] = NULL;
        }

        if (p_sort_table != NULL) {
            // sort
            // write
            // free
        }
        DPRINTF("node count in flush_geodb_table = %d\n", node_count);
        m_hash_tables.p_table_flushing->hash_table_count = 0;
        gettimeofday(&on_end, NULL);

        timersub(&on_end, &on_start, &diff);
        on_start.tv_sec = 10;
        on_start.tv_usec = 0;
        timersub(&on_start, &diff, &diff);
        TIMEVAL_TO_TIMESPEC(&diff, &sleep);
        r = nanosleep(&sleep, &remain);
        while (r == -1) {
            sleep = remain;
            r = nanosleep(&sleep, &remain);
        }
    }

    return 0;
}
